/*
 * Copyright (C) 2010 Tetsuya Yasuda <tetsuyayasuda@gmail.com>
 */
package jp.tyas.libs.lisp;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.ListIterator;

public class Builtin extends SymbolContext
{
  final static String SYMNAME_STDIN = "*STDIN*";
  final static String SYMNAME_STDOUT = "*STDOUT*";

  public Builtin (Reader in, PrintStream out) {
    super (null);

    for (int i = 0; i < callees.length; i++)
      set (new Symbol (callees[i].getName (), null, callees[i]));

    set (new Symbol (SYMNAME_STDIN, in, null));
    set (new Symbol (SYMNAME_STDOUT, out, null));
    set (new Symbol ("OBJECT[].CLASS", Object[].class, null));
    set (new Symbol ("INT[].CLASS", int[].class, null));
    set (new Symbol ("DOUBLE[].CLASS", double[].class, null));
    set (new Symbol ("FLOAT[].CLASS", float[].class, null));
    set (new Symbol ("BYTE[].CLASS", byte[].class, null));
    set (new Symbol ("LONG[].CLASS", long[].class, null));
    set (new Symbol ("SHORT[].CLASS", short[].class, null));
    set (new Symbol ("CHAR[].CLASS", char[].class, null));
  }

  public static List newList () {
    //return new java.util.LinkedList ();
    return new java.util.Vector ();
    //return new java.util.ArrayList();
  }

  public static Reader getStdin (Context ctx) {
    return (Reader) ctx.find("*STDIN*").getValue();
  }

  public static PrintStream getStdout (Context ctx) {
    return (PrintStream) ctx.find("*STDOUT*").getValue();
  }

  public static boolean toBoolean (Object o) {
    return (o instanceof List && ((List)o).size() > 0) ||
      (o != null && !Boolean.FALSE.equals(o));
  }

  public static Object eval (Object sexp, Context ctx) {
    if (sexp instanceof Quoted) {
      return ((Quoted)sexp).getValue ();
    }
        
    else if (sexp instanceof List) {
      List l = (List)sexp;
            
      if (l.size() == 0)
        return Builtin.newList();

      Object fst = l.get (0);
      if (fst instanceof Symbol) {
        Symbol sym = ((Symbol)fst).resolve(ctx);
        if (sym == null)
          throw new LispError ("Symbol not found: " + ((Symbol)fst).getName());
        Callee c = sym.getCallee ();
        if (c != null)
          return c.invoke (l.subList (1, l.size()), ctx);
      }
      else if (fst instanceof Callee) {
        return ((Callee)fst).invoke (l.subList (1, l.size()), ctx);
      }
      else if (fst instanceof List && ((List)fst).get(0) instanceof Symbol && "LAMBDA".equals(((Symbol)((List)fst).get(0)).getName())) {
        List l2 = (List)fst;
        Callee c = new UserFunc ("LAMBDA", (List)l2.get(1), l2.subList(2, l2.size()), ctx);
        return c.invoke (l.subList (1, l.size()), ctx);
      }
      return null;
    }

    else if (sexp instanceof Symbol) {
      return ((Symbol)sexp).resolve(ctx).getValue();
    }
        
    return sexp;
  }

  public static Boolean load (Reader in, Context ctx) {
    try {
      while (true)
        Builtin.eval (Builtin.read(in, ctx), ctx);
    } catch (LispSignal sig) {
      if ("EOF".equals (sig.getMessage()))
        return Boolean.TRUE;
      sig.printStackTrace (Builtin.getStdout(ctx));
    } catch (Exception e) {
      e.printStackTrace (Builtin.getStdout(ctx));
    }
    return Boolean.FALSE;
  }

  public static Object read (Reader in, Context ctx) {
    StreamTokenizer st = new StreamTokenizer (in);

    st.whitespaceChars (9, 13);
    st.whitespaceChars (20, 20);
    st.quoteChar ('"');
    st.commentChar (';');
    st.ordinaryChar ('(');
    st.ordinaryChar (')');
    st.ordinaryChar ('\'');
    st.wordChars (0x2a, 0x2b); // *+
    st.wordChars (0x2d, 0x2d); // -
    st.wordChars (0x2f, 0x2f); // /
    st.wordChars (0x3c, 0x3f);
    st.wordChars (36, 36);
    st.wordChars (91, 91);
    st.wordChars (93, 93);

    return Builtin.readByTokenizer (st, ctx);
  }

  static Object readByTokenizer (StreamTokenizer st, Context ctx)
  {
    List l;
    int type;

    try {
      type = st.nextToken ();
    } catch (IOException e) {
      throw new LispError ("nextToken fail", e);
    }

    switch (type) {
    case StreamTokenizer.TT_EOF:
      throw new LispSignal ("EOF");
    case StreamTokenizer.TT_WORD:
      String upr = st.sval.toUpperCase ();
      if ("NIL".equals (upr))
        return null;
      if ("FALSE".equals (upr))
        return Boolean.FALSE;
      if ("TRUE".equals (upr))
        return Boolean.TRUE;
      return new Symbol (upr);
    case StreamTokenizer.TT_NUMBER:
      return new Double (st.nval);
    case StreamTokenizer.TT_EOL:
      throw new LispError ("EOL detected");
    case '(':
      l = Builtin.newList ();
      try {
        for (;;) l.add (Builtin.readByTokenizer (st, ctx));
      } catch (LispSignal e) {
        if (!")".equals (e.getMessage ()))
          throw e;
      }
      return l;
    case ')':
      throw new LispSignal (")");
    case '\'':
      return new Quoted (Builtin.readByTokenizer (st, ctx));
    case '"':
      return st.sval;
    case '-':
      return ctx.find ("-");
    }
        
    throw new LispError ("Invalid token: \"" + type + "\"");
  }

  public static String getSign (Object o) {
    String res;
    if (o == null)
      res = "NIL";
    else if (o instanceof List) {
      ListIterator it = ((List)o).listIterator ();
      res = "(";
      if (it.hasNext())
        res += Builtin.getSign (it.next());
      while (it.hasNext())
        res += " " + Builtin.getSign (it.next());
      res += ")";
    }
    else if (o instanceof String) {
      res = "\"" + (String)o + "\"";
    }
    else if (o instanceof Number) {
      res = o.toString();
    }
    else if (o instanceof Quoted) {
      res = "'" + Builtin.getSign (((Quoted)o).getValue());
    }
    else if (o instanceof Symbol) {
      res = ((Symbol)o).getName();
    }
    else if (o instanceof Callee) {
      res = o.toString();
    }
    else {
      res = "<" + o.toString () + ">";
    }
    return res;
  }

  public static void print (Object o, PrintStream out) {
    if (out != null) {
      out.print (Builtin.getSign(o));
    }
  }

  public static void println (Object o, PrintStream out) {
    if (out != null) {
      out.println (Builtin.getSign(o));
    }
  }

  public static Object progn (List seq, Context ctx)
  {
    Object res = null;
    int size = seq.size ();
    for (int i = 0; i < size; i++)
      res = Builtin.eval (seq.get(i), ctx);
    return res;
  }

  final static Callee[] callees = new Callee[] {

    new Callee ("EVAL") {
      public Object call (List args, Context ctx) {
        return Builtin.eval (args.get(0), ctx);
      }
    },

    new Callee ("READ") {
      public Object call (List args, Context ctx) {
        return Builtin.read ((Reader)args.get(0), ctx);
      }
    },

    new Callee ("LOAD") {
      public Object call (List args, Context ctx) {
        try {
          return Builtin.load (new FileReader((String)args.get(0)), ctx.getToplevel());
        } catch (FileNotFoundException e) {
          throw new LispError(e);
        }
      }
    },

    new Callee ("PRINT") {
      public Object call (List args, Context ctx) {
        Builtin.print (args.get(0), (args.size()>1 ?
                                     (PrintStream)args.get(1):
                                     Builtin.getStdout(ctx)));
        return args.get(0);
      }
    },

    new Callee ("PRINTLN") {
      public Object call (List args, Context ctx) {
        Builtin.println (args.get(0), (args.size()>1 ?
                                       (PrintStream)args.get(1):
                                       Builtin.getStdout(ctx)));
        return args.get(0);
      }
    },

    new Callee ("WRITE") {
      public Object call (List args, Context ctx) {
        (args.size() > 1 ? (PrintStream)args.get(1): Builtin.getStdout(ctx))
          .print (args.get(0).toString());
        return args.get(0);
      }
    },

    new Callee ("<") {
      public Object call (List args, Context ctx) {
        return Boolean.valueOf(((Number)args.get(0)).doubleValue()
                               < ((Number)args.get(1)).doubleValue());
      }
    },

    new Callee ("+") {
      public Object call (List args, Context ctx) {
        int size = args.size ();
        double res = ((Number)args.get(0)).doubleValue();
        for (int i = 1; i < size; i++)
          res += ((Number)args.get(i)).doubleValue();
        return Double.valueOf(res);
      }
    },

    new Callee ("-") {
      public Object call (List args, Context ctx) {
        int size = args.size ();
        double res = ((Number)args.get(0)).doubleValue();
        if (size == 1)
          return Double.valueOf(-res);
        for (int i = 1; i < size; i++)
          res -= ((Number)args.get(i)).doubleValue();
        return Double.valueOf(res);
      }
    },

    new Callee ("*") {
      public Object call (List args, Context ctx) {
        int size = args.size ();
        double res = ((Number)args.get(0)).doubleValue();
        for (int i = 1; res != 0 && i < size; i++)
          res *= ((Number)args.get(i)).doubleValue();
        return Double.valueOf(res);
      }
    },

    new Callee ("/") {
      public Object call (List args, Context ctx) {
        return Double.valueOf(((Number)args.get(0)).doubleValue() / ((Number)args.get(1)).doubleValue());
      }
    },

    new Callee ("EQ") {
      public Object call (List args, Context ctx) {
        int size = args.size ();
        Object o = args.get(0);
        for (int i = 1; i < size; i++) {
          Object o2 = args.get(i);
          if (!o.equals (o2))
            return Boolean.FALSE;
          o = o2;
        }
        return Boolean.TRUE;
      }
    },

    new Callee ("LIST") {
      public Object call (List args, Context ctx) {
        return args;
      }
    },

    new Callee ("SETQ", false) {
      public Object call (List args, Context ctx) {
        if (args.get(0) instanceof Symbol) {
          Symbol sym = ((Symbol)args.get(0)).resolve(ctx);
          if (sym == null) {
            sym = new Symbol (((Symbol)args.get(0)).getName(), null, null);
            ctx.getToplevel().set(sym);
          }
          sym.setValue (Builtin.eval (args.get(1), ctx));
          return sym;
        }
        throw new LispError ("SETQ fail");
      }
    },

    new Callee ("DEFUN", false) {
      public Object call (List args, Context ctx) {
        Symbol sym = (Symbol)args.get(0);
        sym = ctx.getToplevel().get(sym.getName ());
        if (sym == null) {
          sym = new Symbol (((Symbol)args.get(0)).getName(), null, null);
          ctx.getToplevel().set(sym);
        }
        Callee c = new UserFunc (sym.getName(), (List)args.get(1), args.subList(2, args.size()), ctx);
        sym.setCallee (c);
        return sym;
      }
    },

    new Callee ("DEFSOP", false) {
      public Object call (List args, Context ctx) {
        Symbol sym = (Symbol)args.get(0);
        sym = ctx.getToplevel().get(sym.getName ());
        if (sym == null) {
          sym = new Symbol (((Symbol)args.get(0)).getName(), null, null);
          ctx.getToplevel().set(sym);
        }
        Callee c = new UserFunc (sym.getName(), (List)args.get(1), args.subList(2, args.size()), false, ctx);
        sym.setCallee (c);
        return sym;
      }
    },

    new Callee ("IF", false) {
      public Object call (List args, Context ctx) {
        Object o = Builtin.eval (args.get(0), ctx);

        if (Builtin.toBoolean(o))
          return Builtin.eval (args.get(1), ctx);
                
        if (args.size () >= 3)
          return Builtin.eval (args.get(2), ctx);

        return null;
      }
    },

    new Callee ("LET", false) {
      public Object call (List args, Context ctx) {
        Context lex = new SymbolContext (ctx);
        ListIterator it = ((List)args.get(0)).listIterator();
        while (it.hasNext()) {
          Object obj = it.next();
          String name = null;
          Object value = null;
          if (obj instanceof List) {
            List l = (List)obj;
            name = ((Symbol)l.get(0)).getName();
            value = Builtin.eval (l.get(1), ctx);
          }
          else {
            name = ((Symbol)obj).getName();
          }
          lex.set (new Symbol (name, value, null));
        }
        return Builtin.progn (args.subList (1, args.size()), lex);
      }
    },

    new Callee ("LET*", false) {
      public Object call (List args, Context ctx) {
        Context lex = new SymbolContext (ctx);
        ListIterator it = ((List)args.get(0)).listIterator();
        while (it.hasNext()) {
          Object obj = it.next();
          String name = null;
          Object value = null;
          if (obj instanceof List) {
            List l = (List)obj;
            name = ((Symbol)l.get(0)).getName();
            value = Builtin.eval (l.get(1), lex);
          }
          else {
            name = ((Symbol)obj).getName();
          }
          lex.set (new Symbol (name, value, null));
        }
        return Builtin.progn (args.subList (1, args.size()), lex);
      }
    },

    new Callee ("PROGN", false) {
      public Object call (List args, Context ctx) {
        return Builtin.progn (args, ctx);
      }
    },

    new Callee ("LAMBDA", false) {
      public Object call (List args, Context ctx) {
        return new UserFunc ("LAMBDA", (List)args.get(0), args.subList(1, args.size()), ctx);
      }
    },

    new Callee ("SYMBOLP") {
      public Object call (List args, Context ctx) {
        return Boolean.valueOf((args.get(0) instanceof Symbol));
      }
    },

    new Callee ("LISTP") {
      public Object call (List args, Context ctx) {
        return Boolean.valueOf((args.get(0) instanceof List));
      }
    },

    new Callee ("CALLEEP") {
      public Object call (List args, Context ctx) {
        return Boolean.valueOf((args.get(0) instanceof Callee));
      }
    },

    new Callee ("STRINGP") {
      public Object call (List args, Context ctx) {
        return Boolean.valueOf((args.get(0) instanceof String));
      }
    },

    new Callee ("ARRAYP") {
      public Object call (List args, Context ctx) {
        return Boolean.valueOf(args.get(0).getClass().isArray());
      }
    },

    new Callee ("APPLY") {
      public Object call (List args, Context ctx) {
        Callee c = (Callee)args.get(0);
        int size = args.size();
        List l;
        if (size > 2) {
          l = args.subList (1, size-1);
          l.addAll ((List)args.get(size-1));
        } else {
          l = (List)args.get(1);
        }
        return c.invoke (l, ctx);
      }
    },

    new Callee ("WHILE", false) {
      public Object call (List args, Context ctx) {
        Object sexp = args.get(0);

        while (Builtin.toBoolean (Builtin.eval (sexp, ctx)))
          Builtin.progn (args.subList (1, args.size()), ctx);

        return Boolean.FALSE;
      }
    },

    new Callee ("SYNC") {
      public Object call (List args, Context ctx) {
        Object res;
        synchronized (args.get(0)) {
          res = Builtin.progn (args.subList(1, args.size()), ctx);
        }
        return res;
      }
    },

    new Callee ("TRY") {
      public Object call (List args, Context ctx) {
        Object res;
        try {
          res = Builtin.eval (args.get(0), ctx);
        } catch (Exception e) {
          List l = Builtin.newList();
          l.add ((e instanceof LispError) ? e.getCause() : e);
          res = ((Callee)args.get(1)).invoke (l, ctx);
        }
        return res;
      }
    },

    new Callee ("THROW") {
      public Object call (List args, Context ctx) {
        throw new LispError((Throwable)args.get(0));
      }
    },

    new Callee ("BLOCK", false) {
      public Object call (List args, Context ctx) {
        Object res;
        BlockContext b = new BlockContext (ctx, ((Symbol)args.get(0)).getName());
        try {
          res = Builtin.progn (args.subList(1, args.size()), b);
        } catch (LispSignal sig) {
          if (sig.getData() != b)
            throw sig;
          res = b.getResult();
        }
        return res;
      }
    },

    new Callee ("RETURN-FROM", false) {
      public Object call (List args, Context ctx) {
        String name = ((Symbol)args.get(0)).getName();
        BlockContext b = ctx.findBlock (name);
        if (b == null)
          throw new LispError ("Block " + name + " is not found");
        if (args.size() > 1)
          b.setResult (Builtin.eval (args.get(1), ctx));
        throw new LispSignal ("Block", b);
      }
    },

    new Callee ("PROXY-MEMBER") {
      public Object call (List args, Context ctx) {
        return ProxyDef.getProxyThis(args.get(0));
      }
    },

    new Callee ("DEFPROXY", false) {
      public Object call (List args, Context ctx) {
        String name = ((Symbol)args.get(0)).getName();
        Symbol sym = ctx.getToplevel().get(name);
        if (sym == null) {
          sym = new Symbol(name, null, null);
          ctx.getToplevel().set(sym);
        }
        List l = (List)args.get(1);
        Class[] cls = new Class[l.size()];
        for (int i = 0; i < cls.length; i++)
          cls[i] = (Class)Builtin.eval(l.get(i), ctx);
        ProxyDef def = new ProxyDef(cls);
        ListIterator it = args.listIterator(2);
        while (it.hasNext()) {
          List x = (List)it.next();
          Object key = Builtin.eval(x.get(0), ctx);
          Object val = Builtin.eval(x.get(1), ctx);
          def.put(key, val);
        }
        sym.setValue(def);
        sym.setCallee(new Callee (name) {
            public Object call (List args, Context ctx) {
              ProxyDef def = (ProxyDef)ctx.getToplevel().get(getName()).getValue();
              return def.newInstance();
            }
          });
        return def;
      }
    },

    new Callee ("IMPORT-CLASS") {
      public Object call (List args, Context ctx) {
        String name = (String)args.get(0);
        Class clazz;

        try {
          clazz = Class.forName (name);
        } catch (ClassNotFoundException e) {
          throw new LispError(e);
        }
                
        /**
         * import constructors
         */
        Constructor[] cons = clazz.getConstructors();
        for (int i = 0; i < cons.length; i++) {
          String cname = ("new-" + name).toUpperCase();
          Symbol sym = ctx.getToplevel().get(cname);
          if (sym == null) {
            sym = new Symbol (cname, Builtin.newList(), null);
            ctx.getToplevel().set (sym);
          }
          List mls = (List)sym.getValue();
          mls.add (cons[i]);
          sym.setCallee(new Callee (sym.getName()) {
              public Object call (List args, Context ctx) {
                List mls = (List)ctx.getToplevel().get(getName()).getValue();
                ListIterator it = mls.listIterator();
                try {
                  while (it.hasNext()) {
                    try {
                      Constructor c = (Constructor)it.next();
                      return c.newInstance (args.toArray());
                    } catch (IllegalArgumentException e) {}
                  }
                } catch (IllegalAccessException e) {
                  throw new LispError(e);
                } catch (InvocationTargetException e) {
                  throw new LispError(e);
                } catch (InstantiationException e) {
                  throw new LispError(e);
                }
                throw new LispError ("Invalid Arguments");
              }
            });
        }

        /**
         * import methods
         */
        Method[] methods = clazz.getMethods();
        for (int i = 0; i < methods.length; i++) {
          String mname = (name + "." + methods[i].getName()).toUpperCase();
          Symbol sym = ctx.getToplevel().get (mname);
          if (sym == null) {
            sym = new Symbol (mname, Builtin.newList(), null);
            ctx.getToplevel().set (sym);
          }
          List mls = (List)sym.getValue();
          mls.add (methods[i]);
          sym.setCallee(new Callee (sym.getName()) {
              public Object call (List args, Context ctx) {
                List mls = (List)ctx.getToplevel().get(getName()).getValue();
                ListIterator it = mls.listIterator();
                try {
                  while (it.hasNext()) {
                    try {
                      Method m = (Method)it.next();
                      return m.invoke (args.get(0), args.subList(1, args.size()).toArray());
                    } catch (IllegalArgumentException e) {}
                  }
                } catch (IllegalAccessException e) {
                  throw new LispError(e);
                } catch (InvocationTargetException e) {
                  throw new LispError(e);
                }
                throw new LispError ("Invalid Arguments: " + Builtin.getSign(args.subList(1, args.size())));
              }
            });
        }

        /**
         * import fields
         */
        Field[] fields = clazz.getFields();
        for (int i = 0; i < fields.length; i++) {
          String fname = (name + "." + fields[i].getName()).toUpperCase();
          Symbol sym = ctx.getToplevel().get (fname);
          if (sym == null) {
            sym = new Symbol (fname, null, null);
            ctx.getToplevel().set (sym);
          }
          sym.setValue (fields[i]);
          sym.setCallee (new Callee (sym.getName()) {
              public Object call (List args, Context ctx) {
                try {
                  return ((Field)ctx.getToplevel().get(getName()).getValue()).get(args.get(0));
                } catch (IllegalAccessException e) {
                  throw new LispError(e);
                }
              }
            });
        }

        String cname = name.toUpperCase();
        Symbol sym = ctx.getToplevel().get(cname);
        if (sym == null) {
          sym = new Symbol(cname, null, null);
          ctx.getToplevel().set(sym);
        }
        sym.setValue(clazz);

        return Integer.valueOf(cons.length + methods.length + fields.length);
      }
    },
  };
}
